package drds.common.db.xml_mapping.builder.xml;

import drds.common.db.xml_mapping.builder.BuilderException;
import drds.common.db.xml_mapping.builder.IncompleteElementException;
import drds.common.db.xml_mapping.builder.MapperBuilderAssistant;
import drds.common.db.xml_mapping.parsing.PropertyParser;
import drds.common.db.xml_mapping.parsing.XmlNode;
import drds.common.db.xml_mapping.session.Configuration;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.NodeList;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;


public class XmlIncludeTransformer {

  private final Configuration configuration;
  private final MapperBuilderAssistant mapperBuilderAssistant;

  public XmlIncludeTransformer(Configuration configuration, MapperBuilderAssistant mapperBuilderAssistant) {
    this.configuration = configuration;
    this.mapperBuilderAssistant = mapperBuilderAssistant;
  }

  public void applyIncludes(org.w3c.dom.Node source) {
    Properties variablesContext = new Properties();
    Properties configurationVariables = configuration.getVariables();
    Optional.ofNullable(configurationVariables).ifPresent(variablesContext::putAll);
    applyIncludes(source, variablesContext, false);
  }

  /**
   * Recursively apply includes through all SQL fragments.
   *
   * @param node             Include node in DOM tree
   * @param variablesContext Current context for static variables with values
   */
  private void applyIncludes(org.w3c.dom.Node node, final Properties variablesContext, boolean included) {
    if (node.getNodeName().equals("include")) {
      org.w3c.dom.Node sqlRefXmlNode = findSqlRefXmlNode(getStringAttribute(node, "refId"), variablesContext);
      Properties toIncludeContext = getVariablesContext(node, variablesContext);
      applyIncludes(sqlRefXmlNode, toIncludeContext, true);
      //importNode
      if (sqlRefXmlNode.getOwnerDocument() != node.getOwnerDocument()) {
        sqlRefXmlNode = node.getOwnerDocument().importNode(sqlRefXmlNode, true);
      }
      //replaceNode
      node.getParentNode().replaceChild(sqlRefXmlNode, node);
      while (sqlRefXmlNode.hasChildNodes()) {
        sqlRefXmlNode.getParentNode().insertBefore(sqlRefXmlNode.getFirstChild(), sqlRefXmlNode);
      }
      sqlRefXmlNode.getParentNode().removeChild(sqlRefXmlNode);
      //
    } else if (node.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE) {
      if (included && !variablesContext.isEmpty()) {
        // replace variables in attribute values
        NamedNodeMap attributes = node.getAttributes();
        for (int i = 0; i < attributes.getLength(); i++) {
          org.w3c.dom.Node attr = attributes.item(i);
          attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
        }
      }
      NodeList children = node.getChildNodes();
      for (int i = 0; i < children.getLength(); i++) {
        applyIncludes(children.item(i), variablesContext, included);
      }
    } else if (included && node.getNodeType() == org.w3c.dom.Node.TEXT_NODE
      && !variablesContext.isEmpty()) {
      // replace variables in text node
      node.setNodeValue(PropertyParser.parse(node.getNodeValue(), variablesContext));
    }
  }

  private org.w3c.dom.Node findSqlRefXmlNode(String refId, Properties variables) {
    refId = PropertyParser.parse(refId, variables);
    refId = mapperBuilderAssistant.applyCurrentNamespace(refId, true);
    try {
      XmlNode xmlNode = configuration.getSqlRefIdToXmlNodeMap().get(refId);
      return xmlNode.getNode().cloneNode(true);
    } catch (IllegalArgumentException e) {
      throw new IncompleteElementException("Could not find SQL statement to include with refid '" + refId + "'", e);
    }
  }

  private String getStringAttribute(org.w3c.dom.Node node, String name) {
    return node.getAttributes().getNamedItem(name).getNodeValue();
  }

  /**
   * Read placeholders and their values from include node definition.
   *
   * @param node                      Include node instance
   * @param inheritedVariablesContext Current context used for replace variables in new variables values
   * @return variables context from include instance (no inherited values)
   */
  private Properties getVariablesContext(org.w3c.dom.Node node, Properties inheritedVariablesContext) {
    Map<String, String> declaredProperties = null;
    NodeList children = node.getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      org.w3c.dom.Node n = children.item(i);
      if (n.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE) {
        String name = getStringAttribute(n, "name");
        // Replace variables inside
        String value = PropertyParser.parse(getStringAttribute(n, "value"), inheritedVariablesContext);
        if (declaredProperties == null) {
          declaredProperties = new HashMap<>();
        }
        if (declaredProperties.put(name, value) != null) {
          throw new BuilderException("Variable " + name + " defined twice in the same include definition");
        }
      }
    }
    if (declaredProperties == null) {
      return inheritedVariablesContext;
    } else {
      Properties newProperties = new Properties();
      newProperties.putAll(inheritedVariablesContext);
      newProperties.putAll(declaredProperties);
      return newProperties;
    }
  }
}
